Linux中常见的锁主要有:
互斥锁、读写锁、自旋锁。这三种锁的使用以及区别将在下面一步步深入了解。
1 互斥锁
1.1 互斥锁的作用
互斥锁(也称互斥量)可以用于保护关键代码段,以确保其独占式的访问,和有点像一个二进制信号量。当进入关键代码段时,我们需要获得互斥锁并将其加锁,这等加于二进制信号量的P操作;当离开关键代码段时,我们需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程,这等价于二进制信号量的V操作。
1.2 POSIX互斥锁的相关函数
#include <pthread.h>
下面这些函数的第一个参数mutex指向要操作的目标互斥锁,互斥锁的类型是pthread_mutex_t结构体。
int pthread_mutex_init( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr );用于初始化互斥锁。mutexattr参数指定互斥锁的属性。如果将它设置为NULL,则表示默认属性。
还有一种初始化互斥锁的方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;其中PTHREAD_MUTEX_INITIALIZER是一个结体常量,它只是把互斥锁的各个字段都初始化为0。
int pthread_mutex_destroy( pthread_mutex_t* mutex);该函数用于销毁互斥锁,以释放其占用的内核资源。销毁一个已经枷锁的互斥锁将导致不可预期的后果。
int pthread_mutex_lock( pthread_mutex_t* mutex);该函数以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁上,则pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。
int pthread_mutex_unlock( pthread_mutex_t* mutex);该函数以原子操作的方式给一个互斥锁解锁。如果此时有其他线程正在等待这个互斥锁,则这些线程中的某一个将获得它。
int pthread_mutex_trylock( pthread_mutex_t* mutex);函数pthread_mutex_trylock和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否已经被枷锁。相当于pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock对互斥锁执行加锁操作。当互斥锁已经被加锁时,pthread_mutex_trylock将返回错误码EBUSY。当然,pthread_mutex_trylock和pthread_mutex_lock的行为是针对普通锁而言的。
上面这些函数成功时返回0,失败则返回错误码。
1.3互斥锁的属性
pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作pthread_mutexattr_t类型的变量,以方便我们获取和设置互斥锁属性。
在互斥锁中有两种常用的属性:pshared和type。互斥锁属性pshared指定是否允许跨进程共享互斥锁,其可选值有两个:
PTHREAD_PROCESS_SHARED。互斥锁可以被跨进程共享。
PTHREAD_PROCESS_PRIVATE。互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
互斥锁属性type指定互斥锁的类型。Linux支持4种类型的互斥锁:
(1)PTHREAD_MUTEX_NORMAL,普通锁。这是互斥锁默认的类型。当在该锁解锁后按优先级获得它。这种锁类型保证了资源分配的公平性。但这种锁有个问题就是如果一个线程对一个已经加锁的普通锁再次加锁,将引发死锁。对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
(2)PTHREAD_MUTEX_ERRORCHECK,检错锁。一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK。对一个已经被其他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM。
(3)PTHREAD_MUTEX_RECURSIVE,嵌套锁。这种锁允许一个线程在释放锁之前多次对它加锁而不发生死锁。不过其他线程要想获得这个锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。
(4)PTHREAD_MUTEX_DEFAULT,默认锁。一个线程如果对一个已经加锁的默认锁再次加锁,或者对一个已经被其他线程加锁的默认锁解锁,或者对一个已经解锁的检错锁再次解锁,将导致不可预期的后果。这种锁在实现的时候可能被映射为上面三种锁之一。
2 读写锁
2.1 读写锁得作用
读写锁中的读操作可以共享,写操作是排它的,读可以有多个在读,写只有唯一个在写,写的时候不允许读操作。对于读数据较修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都需要上互斥锁;而采用读写锁则允许在任一时刻多个读出。
2.2 POSIX读写锁的相关函数
#include <pthread.h>
读写锁的初始化有两种方式:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;宏常量初始化。
pthread_rwlock_init(pthread_rwlock_t*, pthread_rwattr_t*);函数初始化。rwlock是读写锁的pthread_rwlock_t结构指针,attr是读写锁的属性结构指针,不需要别的属性默认为NULL。
读写锁的加锁和解锁:
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
获取读写锁的读操作有两种方式:
阻塞式:pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
非阻塞式:pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);如果获取不到锁,会立即返回错误EBUSY。
如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。
pthread_rwlock_unlock(pthread_rwlock_t*);该函数为读写锁的释放。
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);该函数用于销毁读写锁。
2.3 读写锁的属性
读写锁的属性仅支持进程之间的共享。
pthread_rwlockattr_init();用于初始化。
pthread_rwlockattr_destroy();用于回收。成功返回为0,失败则返回错误编号。
pthread_rwlockattr_getpshared();读写锁进程共享属性的查询。
pthread_rwlockattr_setpshared();读写锁进程共享属性的修改。
3 自旋锁
3.1 自旋锁得作用
自旋锁的实现是为了保护一段短小的临界区操作代码,主要是用于在SMP上保护临界区,保证这个临界区的操作是原子的,从而避免并发的竞争冒险。在Linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操作的原子性,在操作这样的结构体时都经历这样的过程:上锁-操作-解锁。如果内核控制路径发现自旋锁“开着”(可以获取),就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。 自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,且操作过程必须短。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。
3.2 POSIX自旋锁的相关函数
自旋锁的初始化有两种方式:
pthread_spin_lock_t lock = SPIN_LOCK_UNLOCKED;自旋锁的宏常量初始化。
pthread_spinlock_init(spin_lock_t* lock)函数初始化。lock是读写锁的spin_lock_t结构指针。
自旋锁的加锁和解锁:
int pthread_spinlock_lock(pthread_spinlock_t* lock);获取一个自旋锁。如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。
int pthread_spinlock_trylock(pthread_spinlock_t* lock);尝试获取一个自旋锁。如果无法获取则理解返回失败。
int pthread_spinlock_unlock(pthread_spinlock_t* lock);用于释放自旋锁。
3.3 自旋锁的属性
pthread_spinlock_init();用于初始化。
pthread_spinlock_lock();用于获取自旋锁。
spin_lock_irqsave(lock, flags);循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。
spin_unlock_irqrestore(lock, flags);将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。
spin_lock_irq(lock);循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0),关掉中断。
spin_unlock_irq(lock);将自旋锁解锁(置为1),打开中断。
spin_unlock_bh(lock);将自旋锁解锁(置为1)。开启底半部的执行。
spin_lock_bh(lock);循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。
pthread_spinlock_destroy();用于回收。成功返回为0,失败则返回错误编号。用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。